/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.catalina.realm;

import java.security.Principal;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.login.AccountExpiredException;
import javax.security.auth.login.CredentialExpiredException;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;

import org.apache.catalina.Container;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.authenticator.Constants;
import org.apache.catalina.util.StringManager;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;

/**
 * <p>
 * Implmentation of <b>Realm</b> that authenticates users via the <em>Java
 * Authentication and Authorization Service</em>
 * (JAAS). JAAS support requires either JDK 1.4 (which includes it as part of
 * the standard platform) or JDK 1.3 (with the plug-in <code>jaas.jar</code>
 * file).
 * </p>
 * 
 * <p>
 * The value configured for the <code>appName</code> property is passed to the
 * <code>javax.security.auth.login.LoginContext</code> constructor, to specify
 * the <em>application name</em> used to select the set of relevant
 * <code>LoginModules</code> required.
 * </p>
 * 
 * <p>
 * The JAAS Specification describes the result of a successful login as a
 * <code>javax.security.auth.Subject</code> instance, which can contain zero
 * or more <code>java.security.Principal</code> objects in the return value of
 * the <code>Subject.getPrincipals()</code> method. However, it provides no
 * guidance on how to distinguish Principals that describe the individual user
 * (and are thus appropriate to return as the value of
 * request.getUserPrincipal() in a web application) from the Principal(s) that
 * describe the authorized roles for this user. To maintain as much independence
 * as possible from the underlying <code>LoginMethod</code> implementation
 * executed by JAAS, the following policy is implemented by this Realm:
 * </p>
 * <ul>
 * <li>The JAAS <code>LoginModule</code> is assumed to return a
 * <code>Subject</code> with at least one <code>Principal</code> instance
 * representing the user himself or herself, and zero or more separate
 * <code>Principals</code> representing the security roles authorized for this
 * user.</li>
 * <li>On the <code>Principal</code> representing the user, the Principal
 * name is an appropriate value to return via the Servlet API method
 * <code>HttpServletRequest.getRemoteUser()</code>.</li>
 * <li>On the <code>Principals</code> representing the security roles, the
 * name is the name of the authorized security role.</li>
 * <li>This Realm will be configured with two lists of fully qualified Java
 * class names of classes that implement <code>java.security.Principal</code> -
 * one that identifies class(es) representing a user, and one that identifies
 * class(es) representing a security role.</li>
 * <li>As this Realm iterates over the <code>Principals</code> returned by
 * <code>Subject.getPrincipals()</code>, it will identify the first
 * <code>Principal</code> that matches the "user classes" list as the
 * <code>Principal</code> for this user.</li>
 * <li>As this Realm iterates over the <code>Princpals</code> returned by
 * <code>Subject.getPrincipals()</code>, it will accumulate the set of all
 * <code>Principals</code> matching the "role classes" list as identifying the
 * security roles for this user.</li>
 * <li>It is a configuration error for the JAAS login method to return a
 * validated <code>Subject</code> without a <code>Principal</code> that
 * matches the "user classes" list.</li>
 * <li>By default, the enclosing Container's name serves as the application
 * name used to obtain the JAAS LoginContext ("Catalina" in a default
 * installation). Tomcat must be able to find an application with this name in
 * the JAAS configuration file. Here is a hypothetical JAAS configuration file
 * entry for a database-oriented login module that uses a Tomcat-managed JNDI
 * database resource: <blockquote>
 * 
 * <pre>
 * Catalina {
 *  org.foobar.auth.DatabaseLoginModule REQUIRED
 *  JNDI_RESOURCE=jdbc/AuthDB
 *  USER_TABLE=users
 *  USER_ID_COLUMN=id
 *  USER_NAME_COLUMN=name
 *  USER_CREDENTIAL_COLUMN=password
 *  ROLE_TABLE=roles
 *  ROLE_NAME_COLUMN=name
 *  PRINCIPAL_FACTORY=org.foobar.auth.impl.SimplePrincipalFactory;
 *  };
 * </pre>
 * 
 * </blockquote></li>
 * <li>To set the JAAS configuration file location, set the
 * <code>CATALINA_OPTS</code> environment variable similar to the following:
 * <blockquote><code>CATALINA_OPTS="-Djava.security.auth.login.config=$CATALINA_HOME/conf/jaas.config"</code></blockquote>
 * </li>
 * <li>As part of the login process, JAASRealm registers its own
 * <code>CallbackHandler</code>, called (unsurprisingly)
 * <code>JAASCallbackHandler</code>. This handler supplies the HTTP
 * requests's username and credentials to the user-supplied
 * <code>LoginModule</code></li>
 * <li>As with other <code>Realm</code> implementations, digested passwords
 * are supported if the <code>&lt;Realm&gt;</code> element in
 * <code>server.xml</code> contains a <code>digest</code> attribute;
 * <code>JAASCallbackHandler</code> will digest the password prior to passing
 * it back to the <code>LoginModule</code></li>
 * </ul>
 * 
 * @author Craig R. McClanahan
 * @author Yoav Shapira
 * @version $Revision: 692717 $ $Date: 2008-09-07 03:52:56 +0800 (星期日, 07 九月
 *          2008) $
 */

public class JAASRealm extends RealmBase {
	private static Log log = LogFactory.getLog(JAASRealm.class);

	// ----------------------------------------------------- Instance Variables

	/**
	 * The application name passed to the JAAS <code>LoginContext</code>,
	 * which uses it to select the set of relevant <code>LoginModule</code>s.
	 */
	protected String appName = null;

	/**
	 * Descriptive information about this <code>Realm</code> implementation.
	 */
	protected static final String info = "org.apache.catalina.realm.JAASRealm/1.0";

	/**
	 * Descriptive information about this <code>Realm</code> implementation.
	 */
	protected static final String name = "JAASRealm";

	/**
	 * The list of role class names, split out for easy processing.
	 */
	protected List<String> roleClasses = new ArrayList<String>();

	/**
	 * The string manager for this package.
	 */
	protected static final StringManager sm = StringManager
			.getManager(Constants.Package);

	/**
	 * The set of user class names, split out for easy processing.
	 */
	protected List<String> userClasses = new ArrayList<String>();

	/**
	 * Whether to use context ClassLoader or default ClassLoader. True means use
	 * context ClassLoader, and True is the default value.
	 */
	protected boolean useContextClassLoader = true;

	// ------------------------------------------------------------- Properties

	/**
	 * setter for the <code>appName</code> member variable
	 * 
	 * @deprecated JAAS should use the <code>Engine</code> (domain) name and
	 *             webpp/host overrides
	 */
	public void setAppName(String name) {
		appName = name;
	}

	/**
	 * getter for the <code>appName</code> member variable
	 */
	public String getAppName() {
		return appName;
	}

	/**
	 * Sets whether to use the context or default ClassLoader. True means use
	 * context ClassLoader.
	 * 
	 * @param useContext
	 *            True means use context ClassLoader
	 */
	public void setUseContextClassLoader(boolean useContext) {
		useContextClassLoader = useContext;
		log.info("Setting useContextClassLoader = " + useContext);
	}

	/**
	 * Returns whether to use the context or default ClassLoader. True means to
	 * use the context ClassLoader.
	 * 
	 * @return The value of useContextClassLoader
	 */
	public boolean isUseContextClassLoader() {
		return useContextClassLoader;
	}

	public void setContainer(Container container) {
		super.setContainer(container);

		if (appName == null) {
			String name = container.getName();
			name = makeLegalForJAAS(name);

			appName = name;

			log.info("Set JAAS app name " + appName);
		}
	}

	/**
	 * Comma-delimited list of <code>java.security.Principal</code> classes
	 * that represent security roles.
	 */
	protected String roleClassNames = null;

	public String getRoleClassNames() {
		return (this.roleClassNames);
	}

	/**
	 * Sets the list of comma-delimited classes that represent roles. The
	 * classes in the list must implement <code>java.security.Principal</code>.
	 * The supplied list of classes will be parsed when {@link #start()} is
	 * called.
	 */
	public void setRoleClassNames(String roleClassNames) {
		this.roleClassNames = roleClassNames;
	}

	/**
	 * Parses a comma-delimited list of class names, and store the class names
	 * in the provided List. Each class must implement
	 * <code>java.security.Principal</code>.
	 * 
	 * @param classNamesString
	 *            a comma-delimited list of fully qualified class names.
	 * @param classNamesList
	 *            the list in which the class names will be stored. The list is
	 *            cleared before being populated.
	 */
	protected void parseClassNames(String classNamesString,
			List<String> classNamesList) {
		classNamesList.clear();
		if (classNamesString == null)
			return;

		ClassLoader loader = this.getClass().getClassLoader();
		if (isUseContextClassLoader())
			loader = Thread.currentThread().getContextClassLoader();

		String[] classNames = classNamesString.split("[ ]*,[ ]*");
		for (int i = 0; i < classNames.length; i++) {
			if (classNames[i].length() == 0)
				continue;
			try {
				Class principalClass = Class.forName(classNames[i], false,
						loader);
				if (Principal.class.isAssignableFrom(principalClass)) {
					classNamesList.add(classNames[i]);
				} else {
					log.error("Class " + classNames[i]
							+ " is not implementing "
							+ "java.security.Principal! Class not added.");
				}
			} catch (ClassNotFoundException e) {
				log.error("Class " + classNames[i]
						+ " not found! Class not added.");
			}
		}
	}

	/**
	 * Comma-delimited list of <code>java.security.Principal</code> classes
	 * that represent individual users.
	 */
	protected String userClassNames = null;

	public String getUserClassNames() {
		return (this.userClassNames);
	}

	/**
	 * Sets the list of comma-delimited classes that represent individual users.
	 * The classes in the list must implement
	 * <code>java.security.Principal</code>. The supplied list of classes
	 * will be parsed when {@link #start()} is called.
	 */
	public void setUserClassNames(String userClassNames) {
		this.userClassNames = userClassNames;
	}

	// --------------------------------------------------------- Public Methods

	/**
	 * Return the <code>Principal</code> associated with the specified
	 * username and credentials, if there is one; otherwise return
	 * <code>null</code>.
	 * 
	 * @param username
	 *            Username of the <code>Principal</code> to look up
	 * @param credentials
	 *            Password or other credentials to use in authenticating this
	 *            username
	 */
	public Principal authenticate(String username, String credentials) {
		return authenticate(username, new JAASCallbackHandler(this, username,
				credentials));
	}

	/**
	 * Return the <code>Principal</code> associated with the specified
	 * username and digest, if there is one; otherwise return <code>null</code>.
	 * 
	 * @param username
	 *            Username of the <code>Principal</code> to look up
	 * @param clientDigest
	 *            Digest to use in authenticating this username
	 * @param nonce
	 *            Server generated nonce
	 * @param nc
	 *            Nonce count
	 * @param cnonce
	 *            Client generated nonce
	 * @param qop
	 *            Quality of protection aplied to the message
	 * @param realmName
	 *            Realm name
	 * @param md5a2
	 *            Second MD5 digest used to calculate the digest MD5(Method +
	 *            ":" + uri)
	 * @param authMethod
	 *            The authentication scheme in use
	 */
	public Principal authenticate(String username, String clientDigest,
			String nonce, String nc, String cnonce, String qop,
			String realmName, String md5a2) {
		return authenticate(username, new JAASCallbackHandler(this, username,
				clientDigest, nonce, nc, cnonce, qop, realmName, md5a2,
				Constants.DIGEST_METHOD));
	}

	// -------------------------------------------------------- Package Methods

	// ------------------------------------------------------ Protected Methods

	/**
	 * Perform the actual JAAS authentication
	 */
	protected Principal authenticate(String username,
			CallbackHandler callbackHandler) {

		// Establish a LoginContext to use for authentication
		try {
			LoginContext loginContext = null;
			if (appName == null)
				appName = "Tomcat";

			if (log.isDebugEnabled())
				log.debug(sm.getString("jaasRealm.beginLogin", username,
						appName));

			// What if the LoginModule is in the container class loader ?
			ClassLoader ocl = null;

			if (!isUseContextClassLoader()) {
				ocl = Thread.currentThread().getContextClassLoader();
				Thread.currentThread().setContextClassLoader(
						this.getClass().getClassLoader());
			}

			try {
				loginContext = new LoginContext(appName, callbackHandler);
			} catch (Throwable e) {
				log.error(sm.getString("jaasRealm.unexpectedError"), e);
				return (null);
			} finally {
				if (!isUseContextClassLoader()) {
					Thread.currentThread().setContextClassLoader(ocl);
				}
			}

			if (log.isDebugEnabled())
				log.debug("Login context created " + username);

			// Negotiate a login via this LoginContext
			Subject subject = null;
			try {
				loginContext.login();
				subject = loginContext.getSubject();
				if (subject == null) {
					if (log.isDebugEnabled())
						log.debug(sm.getString("jaasRealm.failedLogin",
								username));
					return (null);
				}
			} catch (AccountExpiredException e) {
				if (log.isDebugEnabled())
					log.debug(sm
							.getString("jaasRealm.accountExpired", username));
				return (null);
			} catch (CredentialExpiredException e) {
				if (log.isDebugEnabled())
					log.debug(sm.getString("jaasRealm.credentialExpired",
							username));
				return (null);
			} catch (FailedLoginException e) {
				if (log.isDebugEnabled())
					log.debug(sm.getString("jaasRealm.failedLogin", username));
				return (null);
			} catch (LoginException e) {
				log.warn(sm.getString("jaasRealm.loginException", username), e);
				return (null);
			} catch (Throwable e) {
				log.error(sm.getString("jaasRealm.unexpectedError"), e);
				return (null);
			}

			if (log.isDebugEnabled())
				log.debug(sm.getString("jaasRealm.loginContextCreated",
						username));

			// Return the appropriate Principal for this authenticated Subject
			Principal principal = createPrincipal(username, subject);
			if (principal == null) {
				log.debug(sm.getString("jaasRealm.authenticateFailure",
						username));
				return (null);
			}
			if (log.isDebugEnabled()) {
				log.debug(sm.getString("jaasRealm.authenticateSuccess",
						username));
			}

			return (principal);
		} catch (Throwable t) {
			log.error("error ", t);
			return null;
		}
	}

	/**
	 * Return a short name for this <code>Realm</code> implementation.
	 */
	protected String getName() {

		return (name);

	}

	/**
	 * Return the password associated with the given principal's user name. This
	 * always returns null as the JAASRealm has no way of obtaining this
	 * information.
	 */
	protected String getPassword(String username) {

		return (null);

	}

	/**
	 * Return the <code>Principal</code> associated with the given user name.
	 */
	protected Principal getPrincipal(String username) {

		return authenticate(username,
				new JAASCallbackHandler(this, username, null, null, null, null,
						null, null, null, Constants.CERT_METHOD));

	}

	/**
	 * Identify and return a <code>java.security.Principal</code> instance
	 * representing the authenticated user for the specified
	 * <code>Subject</code>. The Principal is constructed by scanning the
	 * list of Principals returned by the JAASLoginModule. The first
	 * <code>Principal</code> object that matches one of the class names
	 * supplied as a "user class" is the user Principal. This object is returned
	 * to tha caller. Any remaining principal objects returned by the
	 * LoginModules are mapped to roles, but only if their respective classes
	 * match one of the "role class" classes. If a user Principal cannot be
	 * constructed, return <code>null</code>.
	 * 
	 * @param subject
	 *            The <code>Subject</code> representing the logged-in user
	 */
	protected Principal createPrincipal(String username, Subject subject) {
		// Prepare to scan the Principals for this Subject

		List<String> roles = new ArrayList<String>();
		Principal userPrincipal = null;

		// Scan the Principals for this Subject
		Iterator<Principal> principals = subject.getPrincipals().iterator();
		while (principals.hasNext()) {
			Principal principal = principals.next();

			String principalClass = principal.getClass().getName();

			if (log.isDebugEnabled()) {
				log.debug(sm.getString("jaasRealm.checkPrincipal", principal,
						principalClass));
			}

			if (userPrincipal == null && userClasses.contains(principalClass)) {
				userPrincipal = principal;
				if (log.isDebugEnabled()) {
					log.debug(sm.getString("jaasRealm.userPrincipalSuccess",
							principal.getName()));
				}
			}

			if (roleClasses.contains(principalClass)) {
				roles.add(principal.getName());
				if (log.isDebugEnabled()) {
					log.debug(sm.getString("jaasRealm.rolePrincipalAdd",
							principal.getName()));
				}
			}
		}

		// Print failure message if needed
		if (userPrincipal == null) {
			if (log.isDebugEnabled()) {
				log.debug(sm.getString("jaasRealm.userPrincipalFailure"));
				log.debug(sm.getString("jaasRealm.rolePrincipalFailure"));
			}
		} else {
			if (roles.size() == 0) {
				if (log.isDebugEnabled()) {
					log.debug(sm.getString("jaasRealm.rolePrincipalFailure"));
				}
			}
		}

		// Return the resulting Principal for our authenticated user
		return new GenericPrincipal(this, username, null, roles, userPrincipal);
	}

	/**
	 * Ensure the given name is legal for JAAS configuration. Added for Bugzilla
	 * 30869, made protected for easy customization in case my implementation is
	 * insufficient, which I think is very likely.
	 * 
	 * @param src
	 *            The name to validate
	 * @return A string that's a valid JAAS realm name
	 */
	protected String makeLegalForJAAS(final String src) {
		String result = src;

		// Default name is "other" per JAAS spec
		if (result == null) {
			result = "other";
		}

		// Strip leading slash if present, as Sun JAAS impl
		// barfs on it (see Bugzilla 30869 bug report).
		if (result.startsWith("/")) {
			result = result.substring(1);
		}

		return result;
	}

	// ------------------------------------------------------ Lifecycle Methods

	/**
	 * 
	 * Prepare for active use of the public methods of this
	 * <code>Component</code>.
	 * 
	 * @exception LifecycleException
	 *                if this component detects a fatal error that prevents it
	 *                from being started
	 */
	public void start() throws LifecycleException {

		// Perform normal superclass initialization
		super.start();

		// These need to be called after loading configuration, in case
		// useContextClassLoader appears after them in xml config
		parseClassNames(userClassNames, userClasses);
		parseClassNames(roleClassNames, roleClasses);
	}

	/**
	 * Gracefully shut down active use of the public methods of this
	 * <code>Component</code>.
	 * 
	 * @exception LifecycleException
	 *                if this component detects a fatal error that needs to be
	 *                reported
	 */
	public void stop() throws LifecycleException {

		// Perform normal superclass finalization
		super.stop();

	}

}
